package com.quick.develop.flowable.service.definition.impl;

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.quick.develop.framework.common.page.PageResult;
import com.quick.develop.framework.utils.PageUtils;
import com.quick.develop.framework.utils.collection.CollectionUtils;
import com.quick.develop.framework.utils.validation.ValidationUtils;
import com.quick.develop.framework.security.utils.SecurityUtils;
import com.quick.develop.flowable.domain.definition.BpmProcessDefinitionCreateReqDTO;
import com.quick.develop.flowable.domain.model.*;
import com.quick.develop.flowable.framework.cmd.UpdateModelInfoCmd;
import com.quick.develop.flowable.framework.convert.model.BpmModelConvert;
import com.quick.develop.flowable.service.definition.IBpmModelService;
import com.quick.develop.flowable.service.definition.IBpmProcessDefinitionService;
import com.quick.develop.flowable.service.task.IBpmPermissionService;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.common.engine.impl.util.io.BytesStreamSource;
import org.flowable.engine.ManagementService;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ModelQuery;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import java.util.*;

import static com.quick.develop.flowable.enums.BpmErrorCodeConstants.*;
import static com.quick.develop.framework.common.utils.ServiceExceptionUtils.exception;
import static com.quick.develop.framework.utils.collection.CollectionUtils.convertMap;

/**
 * @author junyuan.liu
 * @version 1.0
 * @description: 
 * @date 2022/4/21 13:11
 */
@Service
@Validated
@Slf4j
public class BpmModelServiceImpl implements IBpmModelService {
    @Resource
    private RepositoryService repositoryService;

    @Resource
    protected ManagementService managementService;
    
    @Resource
    private IBpmProcessDefinitionService processDefinitionService;
    
    @Resource
    private IBpmPermissionService bpmPermissionService;

    @Override
    public BpmModelRespVO getModel(String id) {
        Model model = repositoryService.getModel(id);
        if (model == null) {
            return null;
        }
        BpmModelRespVO modelRespVO = BpmModelConvert.INSTANCE.convert(model);
        // 拼接 bpmn XML
        byte[] bpmnBytes = repositoryService.getModelEditorSource(id);
        modelRespVO.setBpmnXml(StrUtil.utf8Str(bpmnBytes));
        return modelRespVO;
    }

    @Override
    public Model getModelByKey(String key) {
        return repositoryService.createModelQuery().modelKey(key).singleResult();
    }

    @Override
    public PageResult<BpmModelPageItemRespVO> getModelPage(BpmModelPageReqVO pageVO) {
        ModelQuery modelQuery = repositoryService.createModelQuery();
        if (StrUtil.isNotBlank(pageVO.getKey())) {
            modelQuery.modelKey(pageVO.getKey());
        }
        if (StrUtil.isNotBlank(pageVO.getName())) {
            modelQuery.modelNameLike("%" + pageVO.getName() + "%"); // 模糊匹配
        }
        if (StrUtil.isNotBlank(pageVO.getCategory())) {
            modelQuery.modelCategory(pageVO.getCategory());
        }
        
        Optional.ofNullable(pageVO.getTenantId()).ifPresent(modelQuery::modelTenantId);
        
        // 执行查询
        List<Model> models = modelQuery.orderByCreateTime().desc()
                .listPage(PageUtils.getStart(pageVO), pageVO.getPageSize());

        // 获得 Deployment Map
        Set<String> deploymentIds = new HashSet<>();
        models.forEach(model -> CollectionUtils.addIfNotNull(deploymentIds, model.getDeploymentId()));
        Map<String, Deployment> deploymentMap = processDefinitionService.getDeploymentMap(deploymentIds);
        // 获得 ProcessDefinition Map
        List<ProcessDefinition> processDefinitions = processDefinitionService.getProcessDefinitionListByDeploymentIds(deploymentIds);
        Map<String, ProcessDefinition> processDefinitionMap = convertMap(processDefinitions, ProcessDefinition::getDeploymentId);

        // 拼接结果
        long modelCount = modelQuery.count();
        return new PageResult<>(BpmModelConvert.INSTANCE.convertList(models, deploymentMap, processDefinitionMap), modelCount);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String createModel(BpmModelCreateReqVO createReqVO, String bpmnXml) {
        checkKeyNCName(createReqVO.getKey());
        // 校验流程标识已经存在
        Model keyModel = this.getModelByKey(createReqVO.getKey());
        if (Objects.nonNull(keyModel) ) {
            throw exception(MODEL_KEY_EXISTS, createReqVO.getKey());
        }
        // 创建流程定义
        Model model = repositoryService.newModel();
        BpmModelConvert.INSTANCE.copy(model, createReqVO);
        // TODO TenantId
        // 保存流程定义
        repositoryService.saveModel(model);
        // 保存 BPMN XML
        this.saveModelBpmnXml(model, bpmnXml);
        return model.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateModel(BpmModelUpdateReqVO updateReqVO) {
        // 校验流程模型存在
        Model model = bpmPermissionService.checkModel(updateReqVO.getId());
        // TODO TenantId
        String bpmnXml = updateReqVO.getBpmnXml();
        if (StrUtil.isEmpty(bpmnXml)) {
            managementService.executeCommand(new UpdateModelInfoCmd(model, updateReqVO));
            /*// 只修改流程模型，不修改流程图
            BpmModelConvert.INSTANCE.copy(model, updateReqVO);
            // 更新模型
            repositoryService.saveModel(model);*/
        }else {
            // 更新 BPMN XML 设计流程图的时候不需要更改 model信息
            saveModelBpmnXml(model, bpmnXml);
        }
        
        

    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteModel(String id) {
        // 校验流程模型存在
        Model model = bpmPermissionService.checkModel(id);
        // 执行删除
        repositoryService.deleteModel(id);
        //级联删除流程部署信息
        processDefinitionService.deleteBpmDeployment(model.getDeploymentId(),true);
        // 禁用流程实例
        // 运作中的流程也挂起
        updateProcessDefinitionSuspended(model.getDeploymentId(), true);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveModelBpmnXml(Model model, String bpmnXml) {
        if (StrUtil.isEmpty(bpmnXml)) {
            return;
        }
        repositoryService.addModelEditorSource(model.getId(), StrUtil.utf8Bytes(bpmnXml));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deployModel(String id) {
        // 校验流程模型存在
        Model model = bpmPermissionService.checkModel(id);

        // 校验流程图
        byte[] bpmnBytes = repositoryService.getModelEditorSource(model.getId());
        Optional.ofNullable(bpmnBytes).orElseThrow(() -> exception(MODEL_NOT_EXISTS));

        BpmProcessDefinitionCreateReqDTO definitionCreateReqDTO = BpmModelConvert.INSTANCE.convert2(model);
        definitionCreateReqDTO.setBpmnBytes(bpmnBytes);
        //校验模型是否发生修改。如果未修改，则不允许创建
        if (!processDefinitionService.isProcessDefinitionEquals(definitionCreateReqDTO)) { // 流程定义的信息相等
            throw exception(MODEL_DEPLOY_FAIL_TASK_INFO_EQUALS);
        }
        // 创建流程定义
        String definitionId = processDefinitionService.createProcessDefinition(definitionCreateReqDTO);

        // 将老的流程定义进行挂起。也就是说，只有最新部署的流程定义，才可以发起任务。
        // 老版本--运行中的任务不挂起 
        updateProcessDefinitionSuspended(model.getDeploymentId(), false);

        // 更新 model 的 deploymentId，进行关联
        ProcessDefinition definition = processDefinitionService.getProcessDefinition(definitionId);
        model.setDeploymentId(definition.getDeploymentId());
        repositoryService.saveModel(model);
    }

    @Override
    public void updateModelState(BpmModelUpdateStateReqVO reqVO) {
        Model model = bpmPermissionService.checkModel(reqVO.getId());
        ProcessDefinition definition = bpmPermissionService.checkProcessDefinitionByDeploymentId(model.getDeploymentId());

        // 更新状态
        processDefinitionService.updateProcessDefinitionState(definition.getId(), reqVO.getState(), reqVO.isSuspendProcessInstances());
    }

    @Override
    public BpmnModel getBpmnModelByModelId(String id) {
        byte[] bpmnBytes = repositoryService.getModelEditorSource(id);
        if (ArrayUtil.isEmpty(bpmnBytes)) {
            return null;
        }
        BpmnXMLConverter converter = new BpmnXMLConverter();
        return converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), true, true);
    }

    @Override
    public String getBpmnXmlByModelId(String id) {
        byte[] bpmnBytes = repositoryService.getModelEditorSource(id);
        if (ArrayUtil.isEmpty(bpmnBytes)) {
            return null;
        }
        return StrUtil.utf8Str(bpmnBytes);
    }

    private void checkKeyNCName(String key) {
        if (!ValidationUtils.isXmlNCName(key)) {
            throw exception(MODEL_KEY_VALID);
        }
    }

    /** 
     * @description   挂起 deploymentId 对应的流程定义。 这里一个deploymentId 只关联一个流程定义
     * @param deploymentId 流程发布Id.
     * @param suspendProcessInstances 运行的流程是否挂起. 
     *                                发布新流程的时候不挂起， 
     *                                手动挂起流程模型的时候运行的流程也挂起
     * @author junyuan.liu
     * @date 2022/4/24 15:57
     */ 
    private void updateProcessDefinitionSuspended(String deploymentId,boolean suspendProcessInstances) {
        if (StrUtil.isEmpty(deploymentId)) {
            return;
        }
        ProcessDefinition oldDefinition = processDefinitionService.getProcessDefinitionByDeploymentId(deploymentId);
        if (oldDefinition == null) {
            return;
        }
        processDefinitionService.updateProcessDefinitionState(oldDefinition.getId(), SuspensionState.SUSPENDED.getStateCode(), suspendProcessInstances);
    }
}
